﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Independ
{
    /// <summary>
    /// A simple, extensible dependency injection container.
    /// </summary>
    public class UnityContainer : IUnityContainer, IServiceLocator
    {
        Dictionary<Key, Func<ParameterOverride[], object>> dictionary = new Dictionary<Key, Func<ParameterOverride[], object>>();

        /// <summary>
        /// Register a type mapping with the container.
        /// </summary>
        /// <typeparam name="TFrom"><see cref="Type"/> that will be requested.</typeparam>
        /// <typeparam name="TTo"><see cref="Type"/> that will actually be returned.</typeparam>
        /// <param name="name">Name of this mapping.</param>
        /// <returns>The <see cref="IUnityContainer"/> object that this method was called on.</returns>
        public IUnityContainer RegisterType<TFrom, TTo>(string name = null) where TTo : TFrom
        {
            return RegisterType(typeof(TFrom), typeof(TTo), name);
        }

        /// <summary>
        /// Register a type mapping with the container.
        /// </summary>
        /// <param name="from"><see cref="Type"/> that will be requested.</param>
        /// <param name="to"><see cref="Type"/> that will actually be returned.</param>
        /// <param name="name">Name of this mapping.</param>
        /// <returns>The <see cref="IUnityContainer"/> object that this method was called on.</returns>
        public IUnityContainer RegisterType(Type from, Type to, string name = null)
        {
            return Register(new Key(from, name), (ps) => CreateInstance(to, name, ps));
        }

        /// <summary>
        /// Register an instance with the container.
        /// </summary>
        /// <typeparam name="TInterface">Type of instance to register (may be an implemented interface instead of the full type).</typeparam>
        /// <param name="instance">Object to returned.</param>
        /// <param name="name">Name for registration.</param>
        /// <returns>The <see cref="IUnityContainer"/> object that this method was called on.</returns>
        public IUnityContainer RegisterInstance<TInterface>(TInterface instance, string name = null)
        {
            return Register(new Key(typeof(TInterface), name), (ps) => instance);
        }

        /// <summary>
        /// Register func with the container.
        /// </summary>
        /// <typeparam name="T"><see cref="Type"/> that will actually be returned.</typeparam>
        /// <param name="func">Func that will return <typeparamref name="T"/>.</param>
        /// <param name="name">Name for registration.</param>
        /// <returns>The <see cref="IUnityContainer"/> object that this method was called on.</returns>
        public IUnityContainer Register<T>(Func<IUnityContainer, T> func, string name = null)
        {
            return Register(new Key(typeof(T), name), ps => func.Invoke(this));
        }

        IUnityContainer Register(Key key, Func<ParameterOverride[], object> func)
        {
            dictionary.Remove(key);
            dictionary.Add(key, func);
            return this;
        }

        /// <summary>
        /// Resolve an instance of the default requested type from the container.
        /// </summary>
        /// <typeparam name="T"><see cref="Type"/> of object to get from the container.</typeparam>
        /// <param name="name">Name of the object to retrieve.</param>
        /// <param name="resolverOverrides">Any overrides for the resolve call.</param>
        /// <returns>The retrieved object.</returns>
        public T Resolve<T>(string name = null, params ParameterOverride[] resolverOverrides)
        {
            return (T)Resolve(typeof(T), name, resolverOverrides);
        }

        /// <summary>
        /// Resolve an instance of the requested type with the given name from the container.
        /// </summary>
        /// <param name="t"><see cref="Type"/> of object to get from the container.</param>
        /// <param name="name">Name of the object to retrieve.</param>
        /// <param name="resolverOverrides">Any overrides for the resolve call.</param>
        /// <returns>The retrieved object.</returns>
        public object Resolve(Type t, string name = null, params ParameterOverride[] resolverOverrides)
        {
            return ResolveImp(t, name, resolverOverrides);
        }

        /// <summary>
        /// Return instances of all registered types requested.
        /// </summary>
        /// <para>This method is useful if you've registered multiple types with the same <see cref="Type"/> but different names.</para>
        /// <param name="t">The type requested.</param>
        /// <returns>Set of objects of type <paramref name="t"/>.</returns>
        public IEnumerable<object> ResolveAll(Type t)
        {
            return dictionary.Where(k => k.Key.Type == t).Select(e => e.Value.Invoke(new ParameterOverride[] { }));
        }

        /// <summary>
        /// Return instances of all registered types requested.
        /// </summary>
        /// <typeparam name="T"><see cref="Type"/> of object to get from the container.</typeparam>
        /// <returns>Set of objects of type <typeparamref name="T"/>.</returns>
        public IEnumerable<T> ResolveAll<T>()
        {
            return ResolveAll(typeof(T)).Cast<T>();
        }

        /// <summary>
        /// Check if a particular type/name pair has been registered with the container.
        /// </summary>
        /// <param name="type">Type to check registration for.</param>
        /// <param name="name">Name to check registration for.</param>
        /// <returns>True if this type has been registered, false if not.</returns>
        public bool IsRegistered(Type type, string name = null)
        {
            return dictionary.ContainsKey(new Key(type, name));
        }

        /// <summary>
        /// Check if a particular type/name pair has been registered with the container.
        /// </summary>
        /// <typeparam name="T">Type to check registration for.</typeparam>
        /// <param name="name">Name to check registration for.</param>
        /// <returns>True if this type has been registered, false if not.</returns>
        public bool IsRegistered<T>(string name = null)
        {
            return IsRegistered(typeof(T), name);
        }

        object ResolveImp(Type type, string name, ParameterOverride[] resolverOverrides)
        {
            var key = new Key(type, name);
            if (dictionary.ContainsKey(key))
            {
                return dictionary[key].Invoke(resolverOverrides);
            }
            if ((type.IsClass || type.IsValueType) && !type.IsAbstract)
            {
                return CreateInstance(type, name, resolverOverrides);
            }
            throw new ContainerException();
        }

        object CreateInstance(Type type, string name, ParameterOverride[] resolverOverrides)
        {
            ConstructorInfo constructor;
            var injectionConstructors = type.GetConstructors().Where(c => c.GetCustomAttributes(typeof(InjectionConstructorAttribute), false).Length > 0).ToList();
            if (injectionConstructors.Count == 0)
            {
                if (type.GetConstructors().Length == 0 && type.IsValueType)
                {
                    return SetDependencyProperties(Activator.CreateInstance(type));
                }
                var maxParameters = type.GetConstructors().Max(c => c.GetParameters().Length);
                var constructors = type.GetConstructors().Where(c => c.GetParameters().Length == maxParameters).ToList();
                if (constructors.Count > 1)
                {
                    throw new ContainerException();
                }
                constructor = constructors[0];
            }
            else if (injectionConstructors.Count == 1)
            {
                constructor = injectionConstructors[0];
            }
            else
            {
                throw new ContainerException();
            }
            var ps = constructor.GetParameters();
            return SetDependencyProperties(constructor.Invoke(ps.Select(p =>
                {
                    var value = resolverOverrides.FirstOrDefault(o => o.Name == p.Name);
                    if (value != null)
                    {
                        return value.Value;
                    }
                    return ResolveImp(p.ParameterType, name, new ParameterOverride[] { });
                }
                ).ToArray()));
        }

        object SetDependencyProperties(object instance)
        {
            var propertiesWithDependencyAttribute =
                instance.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(DependencyAttribute), false).Length > 0);
            foreach (var item in propertiesWithDependencyAttribute)
            {
                var dependencyAttribute = item.GetCustomAttributes(typeof(DependencyAttribute), false)[0] as DependencyAttribute;
                item.SetValue(instance, ResolveImp(item.PropertyType, dependencyAttribute.Name, new ParameterOverride[] { }), null);
            }
            return instance;
        }

        #region IServiceLocator

        object IServiceLocator.GetInstance(Type serviceType)
        {
            return Resolve(serviceType);
        }

        object IServiceLocator.GetInstance(Type serviceType, string key)
        {
            return Resolve(serviceType, key);
        }

        IEnumerable<object> IServiceLocator.GetAllInstances(Type serviceType)
        {
            return ResolveAll(serviceType);
        }

        TService IServiceLocator.GetInstance<TService>()
        {
            return Resolve<TService>();
        }

        TService IServiceLocator.GetInstance<TService>(string key)
        {
            return Resolve<TService>(key);
        }

        IEnumerable<TService> IServiceLocator.GetAllInstances<TService>()
        {
            return ResolveAll<TService>();
        }

        object IServiceProvider.GetService(Type serviceType)
        {
            return Resolve(serviceType);
        }

        #endregion
    }

    struct Key
    {
        public Key(Type type, string name = null)
            : this()
        {
            Type = type;
            Name = name;
        }

        public Type Type { get; private set; }

        public string Name { get; private set; }
    }
}
